/*
 * backtrace_test.c
 *
 *  Created on: Dec 28, 2010
 *      Author: yossi
 */

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <execinfo.h>
#include <bfd.h>
#include <libiberty.h>
#include <dlfcn.h>
#include <link.h>

#include "backtrace.h"

#define BACKTRACE_MAX 64


struct backtrace_line {
	unsigned long            address;
	char                     *file;
	char                     *function;
	unsigned                 lineno;
};

struct backtrace_file {
	unsigned long            address;
	const char               *filename;
	unsigned long            base;
	bfd                      *abfd;
	asymbol                  **syms;
};

struct backtrace {
	struct backtrace_line    lines[BACKTRACE_MAX];
	int                      size;
	int                      position;
};

struct backtrace_search {
	int                      count;
	struct backtrace_file    *file;
	struct backtrace_line    *lines;
	int                      max_lines;
};


static int dl_match_address(struct dl_phdr_info *info, size_t size, void *data)
{
	struct backtrace_file *file = data;
	const ElfW(Phdr) *phdr;
	ElfW(Addr) load_base = info->dlpi_addr;
	long n;

	phdr = info->dlpi_phdr;
	for (n = info->dlpi_phnum; --n >= 0; phdr++) {
		if (phdr->p_type == PT_LOAD) {
			ElfW(Addr) vbaseaddr = phdr->p_vaddr + load_base;
			if (file->address >= vbaseaddr && file->address < vbaseaddr + phdr->p_memsz) {
				file->filename = info->dlpi_name;
				file->base = info->dlpi_addr;
			}
		}
	}
	return 0;
}

static int load_file(void *address, struct backtrace_file *file)
{
	long symcount;
	unsigned int size;
	char **matching;
	char **ret_buf;

	file->address = (unsigned long)address;
	file->filename = "";
	file->base = 0;
	file->syms = NULL;
	file->abfd = NULL;
	dl_iterate_phdr(dl_match_address, file);
	if (!strlen(file->filename))
		file->filename = "/proc/self/exe";

	file->abfd = bfd_openr(file->filename, NULL);
	if (!file->abfd)
		goto err;

	if (bfd_check_format(file->abfd, bfd_archive))
		goto err_close;

	if (!bfd_check_format_matches(file->abfd, bfd_object, &matching))
		goto err_close;

	if ((bfd_get_file_flags(file->abfd) & HAS_SYMS) == 0)
		goto err_close;

	symcount = bfd_read_minisymbols(file->abfd, 0, (PTR)&file->syms, &size);
	if (symcount == 0)
		symcount = bfd_read_minisymbols(file->abfd, 1, (PTR)&file->syms, &size);
	if (symcount < 0)
		goto err_close;

	return 1;

err_close:
	bfd_close(file->abfd);
err:
	return 0;
}

static void unload_file(struct backtrace_file *file)
{
	free(file->syms);
	bfd_close(file->abfd);
}

static void find_address_in_section(bfd *abfd, asection *section, void *data)
{
	struct backtrace_search *search = data;
	bfd_size_type size;
	bfd_vma vma;
	unsigned long address;
	const char *filename, *function;
	unsigned lineno;
	int found;

	if (search->count)
		return;

	if ((bfd_get_section_flags(abfd, section) & SEC_ALLOC) == 0)
		return;

	address = search->file->address - search->file->base;
	vma = bfd_get_section_vma(abfd, section);
	if (address < vma)
		return;

	size = bfd_section_size(abfd, section);
	if (address >= vma + size)
		return;

	if (search->max_lines == 0)
		return;

	/* Search in address-1 to get the calling line instead of return address */
	found = bfd_find_nearest_line(abfd, section, search->file->syms, address - vma - 1,
	                              &filename, &function, &lineno);
	do {
		search->lines[search->count].address = address;
		search->lines[search->count].file = filename ? strdup(filename) : NULL;
		search->lines[search->count].function = function ? strdup(function) : NULL;
		search->lines[search->count].lineno = lineno;
		if (!search->count) {
			/* To get the inliner info, search at the original address */
			bfd_find_nearest_line(abfd, section, search->file->syms, address - vma,
			                      &filename, &function, &lineno);
		}
		++search->count;

		found = bfd_find_inliner_info(abfd, &filename, &function, &lineno);
	} while (found && search->count < search->max_lines);
}

static int get_line_info(struct backtrace_file *file,
                         struct backtrace_line *lines, int max)
{
	struct backtrace_search search;

	search.file = file;
	search.count = 0;
	search.lines = lines;
	search.max_lines = max;
	bfd_map_over_sections(file->abfd, find_address_in_section, &search);
	return search.count;
}

backtrace_t * backtrace_create(void)
{
	struct backtrace_file file;
	void *addresses[BACKTRACE_MAX];
	int i, num_addresses;
	backtrace_t *bckt;

	bckt = malloc(sizeof *bckt);
	if (!bckt)
		return NULL;

	num_addresses = backtrace(addresses, BACKTRACE_MAX);

	bfd_init();

	bckt->size = 0;
	for (i = 0; i < num_addresses; ++i) {
		if (load_file(addresses[i], &file)) {
			bckt->size += get_line_info(&file, bckt->lines + bckt->size,
										BACKTRACE_MAX - bckt->size);
			unload_file(&file);
		}
	}

	bckt->position = 0;
	return bckt;
}

void backtrace_destroy(backtrace_t *bckt)
{
	int i;

	for (i = 0; i < bckt->size; ++i) {
		free(bckt->lines[i].function);
		free(bckt->lines[i].file);
	}
	free(bckt);
}

int backtrace_next(backtrace_t *bckt, unsigned long *address, char const ** file,
		char const ** function, unsigned *lineno)
{
	struct backtrace_line *line;

	if (bckt->position >= bckt->size)
		return 0;

	line = &bckt->lines[bckt->position++];
	*address = line->address;
	*file = line->file;
	*function = line->function;
	*lineno = line->lineno;
	return 1;
}

void backtrace_print(FILE *stream)
{
	backtrace_t *bckt;
	unsigned long address;
	const char *file, *function;
	unsigned line;
	int i;

	bckt = backtrace_create();

    fprintf(stream, "==== backtrace ====\n");
    i = 0;
    while (backtrace_next(bckt, &address, &file, &function, &line)) {
    	fprintf(stream, "%2d 0x%016lx %s()  %s:%u\n", i, address,
    	        function ? function : "??", file ? file : "??", line);
    	++i;
    }
    fprintf(stream, "===================\n");

    backtrace_destroy(bckt);
}
